Inhalt: Informationen ber die Benutzung von Prozessen und Threads
        (Programmterminierung, Speicherverwaltung usw. (wichtig!)).


Programme, Prozesse, Threads und deren Beendigung
=================================================
Die Beendigung eines Programms hrt sich erst mal nach nix Schwierigem an;
das habe ich zuerst auch gedacht, bin jedoch bei der Programmierung der
Modulterminierung auf immer mehr Probleme gestoen. Deshalb soll hier
einmal eine Zusammenfassung gegeben werden, was dabei alles zu beachten
ist (Meine Definitionen von Programm, Hauptproze, Unterproze und Thread
stimmen dabei vielleicht nicht ganz mit dem Gngigen berein, aber fr die
Erklrung reicht's):

Ein Programm besteht aus einem oder mehreren Prozessen, die mit dem
gleichen Programmcode arbeiten. Existieren mehrere Prozesse, heien diese
auch Threads. Beim Programmstart existiert genau ein Proze, der
Hauptproze; neue Prozesse (Unterprozesse) knnen mit "proc.fork()",
"proc.vfork()" oder "proc.tfork()" erzeugt werden. Bei "fork()" bekommt
der neue Proze lediglich eine Kopie des Speicherbereichs des aufrufenden
Prozesses (bis auf das TEXT-Segment, das gemeinsam benutzt wird). Somit haben
die beiden Prozesse keinen gemeinsamen Speicher und Vernderungen an globalen
Variablen des einen Prozesses beeinflussen nicht die Variablen des anderen
Prozesses. Bei "vfork()" und "tfork()" benutzen die Prozesse einen
gemeinsamen Speicherbereich. Vernderungen an den Variablen des einen
Prozesses betreffen auch die Variablen des anderen Prozesses.

Ein Programm ist erst dann beendet, wenn jeder seiner Prozesse beendet
ist. Dabei ist es egal, ob der Hauptproze oder einer seiner
Unterprozesse als letzter das Programm beendet! Da der Hauptproze
als letzter beendet wird, ist der Normalfall. Wird dagegen der
Hauptproze vor seinem Unterproze beendet, spricht man von einem
Hintergrund- oder Dmon-Proze; in diesem Fall wird der ehemalige
Unterproze zum Hauptproze.

D.h. Hauptproze ist derjenige Proze, mit dessen Ende auch das Programm
beendet wird (= Definition).

Bei der Beendigung des Programms -- und nur dann -- knnen automatisch
bestimmte Routinen ausgefhrt werden. Dabei ist zu unterscheiden zwischen
den mit "atexit()" vom Benutzer/Programmierer installierten Routinen, und
denen, die vom Laufzeitsystem oder der Systembibliothek selbst installiert
wurden (Systemterminierungen).

Beispiele fr Systemterminierungen sind die Modulterminierung bei Megamax,
die im etv_term-Vektor hngt und bei Programmende wieder entfernt werden
mu (aber auf keinen Fall vorher), oder auch das Restaurieren von
(ohne XBRA) verbogenen Systemvektoren im Laufzeitsystem von SPC und TDI.

Systemterminierungen mssen(!) beim Programmende ausgefhrt werden; die
vom Benutzer mit "atexit()" installierten Routinen drfen(!) dagegen
beim Programmende ausgefhrt werden. Bei Beendigung eines Prozesses, der
nicht auch gleichzeitig das Programmende darstellt, drfen keinerlei
Terminierungsroutinen ausgefhrt werden!

Die Schwierigkeiten entstehen dadurch, da man auf die vom System
installierten Routinen keinen Einflu hat. In `C' macht das keine
Schwierigkeiten, da hier ausschlielich ber atexit installiert wird,
es gibt normalerweise keine speziellen Systemterminierungen.


Die folgenden Routinen haben mit der Programmterminierung zu tun:
-----------------------------------------------------------------
"DosSystem.IsMain()": Liefert TRUE, wenn der aufrufende Proze der
Hauptproze ist, d.h. derjenige, mit dessen Ende auch das Programm
beendet ist. Das ist normalerweise der bei Programmstart aktive
Proze. Diese Prozedur ist normalerweise fr den Programmierer
uninteressant, sie wird aber von den unten beschriebenen exit-Routinen
verwendet, um zu entscheiden ob Terminierungsroutinen ausgefhrt
werden sollen.

"DosSystem.SetMain()": Kennzeichnet den aufrufenden Proze als
Hauptproze. Es liegt in der Verantwortung des Programmierers dafr
zu sorgen, da es dann auch wirklich dieser Proze ist, der das
Programm beendet! Die Anwendung dieser Prozedur beschrnkt sich
auf die Kennzeichnung eines Hintergrundprozesses als Hauptproze,
nachdem der erzeugende Proze verschwunden ist.

"DosSystem.ExitSys()": Bei Ausfhrung dieser Prozedur wird der aufrufende
Proze beendet, ohne da irgendwelche Terminierungsroutinen ausgefhrt
werden. Es wird dabei nicht berprft, ob die Prozedur vom Hauptproze
ausgefhrt wird oder nicht. Die Anwendung dieser Prozedur beschrnkt
sich auf die Selbst-Terminierung des Hauptprozesses nach Erzeugen
eines Hintergrundprozesses (Dmon).

"DosSystem.atexit()": Installiert eine Prozedur, die bei Ende des
Hauptprozesses automatisch ausgefhrt wird. Dies kann fr ein automatisches
``Aufrumen'' verwendet werden.

"DosSystem.Exit()": Wenn fr den aufrufenden Proze IsMain() = TRUE gilt,
werden die Systemterminierungen, nicht jedoch die mit "atexit()"
installierten Routinen ausgefhrt. Auf jeden Fall wird der Proze
dann beendet. Diese Routine ist normalerweise fr die Terminierung eines
Unterprozesses gedacht, kann jedoch auch vom Hauptproze verwendet werden,
wenn irgendwas schiefgelaufen ist, da die normale Programmterminierung
ungeeignet erscheinen lt. Korrekterweise mte die Prozedur "_exit()"
lauten, aber der Unterstrich in Modula...

"DosSystem.exit()": Wenn fr den aufrufenden Proze IsMain() = TRUE gilt,
werden sowohl die Systemterminierungen als auch die mit "atexit()"
installierten Routinen in umgekehrter Reihenfolge ihrer Installation
ausgefhrt. Auf jeden Fall wird der Proze dann beendet. Diese Routine
ist ausschlielich fr die explizite Beendigung des Hauptprozesses gedacht,
wenn nicht das normale Programmende durch Erreichen des Hauptmodulendes
gewnscht ist. Die interne Abfrage mit "IsMain()" wird nur sicherheitshalber
gemacht!

Normalerweise kann also ``C''-like mit "exit()" und "Exit()" programmiert
werden, nur bei Hintergrundprozessen ist eine etwas andere Vorgehensweise
ntig. Zwei Beispiele fr Hintergrundprozesse sind die Programme 'Daemon1'
und 'Daemon2'.


Terminierung durch Signale
==========================
Die Beendigung eines Prozesses bzw. Programms durch ein Signal ist eng
mit den obigen Ausfhrungen verknpft. Die meisten Signale, die nicht entweder
ignoriert werden oder fr die kein Handler installiert ist (--> "sigaction()",
"signal()"), beenden den Proze, dem dieses Signal gesendet wurde. Handelt es
sich bei dem Proze um den Hauptproze, wird also durch dessen Ende auch das
Programm beendet, und hat der Proze irgendwelche Resourcen angefordert oder
sich gar in Vektoren eingehngt, so sollte dies vor dem Ende des Programms
``bereinigt'' werden. Dazu zhlen natrlich auch die oben erwhnten
Systemterminierungen. Es sollte also mindestens fr das Signal SIGTERM und die
``interaktiven'' Signale SIGINT (CTRL-C bzw. CTRL-ALT-C) und SIGQUIT
(SHIFT-CTRL-ALT-\) und evtl. auch fr SIGHUP ein Handler installiert werden,
der fr die ntigen Aufrumarbeiten sorgt. Fr die Systeme SPC und TDI sollten
beispielsweise immer Handler installiert sein, die wenigstens ein "Exit()"
oder "exit()" ausfhren, damit die Vektoren, in die sich die Laufzeitsysteme
eingehngt haben, wieder restauriert werden.

Wenn von einem Programm keine Resourcen angefordert und keine Vektoren
verndert wurden, brauchen die Signale natrlich nicht abgefangen zu werden.
Es ist es auch mglich, die Signale einfach zu ignorieren (SigIgn als
``Handler'' installieren) oder zeitweise zu blockieren; es besteht dann
allerdings die Gefahr, da der Benutzer ungeduldig wird und ein SIGKILL
verschickt, das dann das Programm auf jeden Fall beendet, ohne da es
allerdings vorher die Chance hat, irgendetwas zu restaurieren.

Was innerhalb eines Handlers getan werden darf, hngt davon ab, ob es sich
um ein TOS- oder GEM-Programm handelt. Auf jeden Fall drfen beliebige BIOS-,
XBIOS- und GEMDOS-Funktionen aufgerufen werden; in einem TOS-Programm ist es
also mglich, smtliche Aufrumarbeiten innerhalb des Handlers zu erledigen
und dann das Programm mittels "Exit()" oder "exit()" zu beenden. Man knnte
allerdings auch den Handler mittels "longjmp()" verlassen und zu einem
festgelegten Punkt im Programm springen, um dort alles weitere erledigen.
In einem GEM-Programm sieht es jedoch anders aus. Das GEM ist nicht reentrant,
deswegen drfen innerhalb eines Signalhandlers keine GEM-Aufrufe stattfinden;
schlielich knnte gerade eine GEM-Funktion durch das Signal unterbrochen
worden sein (was sehr wahrscheinlich ist, denn die meiste Zeit wartet das
Programm ja auf ein Ereignis mittels einer Event-Funktion). Es mu also
sichergestellt sein, da der letzte GEM-Aufruf beendet ist, bevor man daran
geht Resource-Dateien freizugeben oder die virtuelle Workstation zu schlieen.
Damit entfllt dann allerdings auch die Benutzung von "longjmp()". Alles was
innerhalb des Handlers getan werden kann, ist, ein Flag zu setzen, das ein
gewnschtes Programmende signalisiert. Innerhalb der (Multi)Event-Schleife
wird dann ein zustzliches Zeitereignis eingeplant, so da z.B. jede Sekunde
dieses Flag abgefragt werden kann. Wurde das Flag gesetzt, kann man wie bei
einem normalen Programmende fortfahren, wobei man vielleicht auf evtl.
Rckfragen ("Soll das Programm beendet werden?") verzichten sollte.
(Siehe auch ATARIUM ST-C 5/96.)


Threads und Speicherverwaltung
==============================
Allgemein fr Speicherverwaltungen gilt: Wenn ein Programm per "ALLOCATE()"
oder "malloc()" o.. Speicher anfordert, wird gegebenenfalls per Malloc() ein
Speicherblock beim Betriebssystem angefordert. Dieser Speicherblock wird dann
intern in einer Liste oder einer anderen Struktur mit den brigen durch
Malloc() angeforderten Speicherblcken verbunden. Diese Speicherblcke haben
immer eine gewisse Mindestgre, damit die Speicherverwaltung des
Betriebssystems nicht durch viele kleine, nur wenige Bytes groe
Speicheranforderungen belastet wird (jede Speicheranforderung bedeutet auch
internen Verwaltungsaufwand). Wenn in dem Speicherblock noch gengend Platz
ist, kann das nchste ALLOCATE ebenfalls durch Reservieren innerhalb dieses
Blocks erfllt werden, so da nicht das Betriebssystem bemht werden mu.

Die per Malloc() beim Betriebssystem angeforderten Speicherblcke haben als
Besitzer den aufrufenden Proze, der bei MiNT ber die Prozekennung und
bei TOS (auch MagiC?) ber die Adresse der Basepage gekennzeichnet ist.
Wenn ein Proze endet, werden Speicherblcke, die nicht explizit mit Mfree()
zurckgegeben wurden, automatisch freigegeben.

Gibt es in einem Programm mehrere Threads, die Speicher anfordern, besteht
folgendes Problem: Wird per Malloc() ein Speicherblock beim Betriebssystem
angefordert, gehrt er dem gerade aktiven Thread. Dieser Speicherblock
wird mit den anderen Speicherblcken, unabhngig davon, welchem Thread sie
gehren, ber Zeiger verknpft. Ist auerdem in dem gerade angeforderten
Speicherblock noch gengend Platz, wird die nchste Speicheranforderung,
auch wenn sie von einem anderen Thread kommt, aus diesem Speicherblock
erfllt. Damit verweisen Zeiger unterschiedlicher Threads in diesen
Speicherblock. Auch wenn der Thread, dem dieser Speicherblock gehrt,
seinen Speicher explizit ber die Speicherverwaltung wieder freigegeben hat,
kann der Speicherblock nicht an's Betriebssystem zurckgegeben werden, wenn
nicht auch die anderen Threads den Speicher, der innerhalb dieses Blocks
liegt, freigegeben haben. Wird jetzt der Thread, dem der Speicherblock
gehrt, beendet, wird der Speicherblock automatisch vom Betriebssystem
freigegeben. Damit verweisen dann die Zeiger der anderen Threads in's
Leere! Dieses Problem besteht auch unter TOS, da bei "tfork()" und "vfork()"
neue Basepages und damit neue Prozesse erzeugt werden.

Die einzige Mglichkeit, dieses Problem zu verhindern, ist, fr jeden Thread
eine getrennte Speicherverwaltung einzurichten, so da smtliche
Speicheranforderungen aus Speicherblcken erfllt werden, die dem Thread
selbst gehren.

Nun kann nicht fr jeden mglichen Proze eine solche getrennte Verwaltung im
voraus eingerichtet werden, wobei dann bereits alles vorbereitet ist, wenn
der Thread zum ersten Mal Speicher anfordert, und nach der letzten
Speicheranforderung die Verwaltung im derzeitigen Zustand zurckbleibt. Zum
einen ist die Zahl der mglichen Prozekennungen (unter TOS aus der
Basepageadresse berechnet) sehr hoch (32767), zum anderen knnen
Prozekennungen auch wiederverwendet werden (unter MiNT unwahrscheinlich,
nicht aber unter MagiC und TOS), wodurch es unmglich ist, zu entscheiden, ob
die Verwaltungsstrukturen vom jetzigen Thread stammen oder von einem frheren
mit gleicher Prozekennung.

Aus diesem Grund mssen sich Threads explizit bei der Speicherverwaltung
vor der ersten Speicheranforderung anmelden und nach der letzten
Speicheranforderung wieder abmelden. Es kann auerdem nur eine begrenzte
Anzahl Threads gleichzeitig angemeldet sein. Es gibt zwar fr jeden
angemeldeten Thread eine separate Speicherverwaltung, allerdings ist die
Verwaltung der einzelnen Threads global. Deshalb werden die Threads beim
Aufruf von Prozeduren aus 'mem' ber eine Semaphore synchronisiert, die beim
Programmstart angelegt und beim Programmende ber eine Modulterminierung
wieder entfernt wird. Unter TOS existiert zwar der Psemaphore()-Aufruf nicht,
aber es gibt auch keine parallelen Prozesse, weswegen eine Synchronisation
nicht erforderlich ist. MagiC kennt zwar ab Version 3 Semaphoren, aber die
erweiterten Pexec()-Modi (noch) nicht, weswegen durch die "*fork()"-Aufrufe
keine parallelen Prozesse erzeugt werden.

Angemeldet wird ein Thread durch "mem.RegisterMe()" oder
"mem.RegisterThread()"; abgemeldet wird er durch "mem.UnregisterMe()" oder
"mem.UnregisterThread()". Mit den "*Me()"-Prozeduren kann sich ein Thread
selbst an- und abmelden, was der bliche Fall sein drfte, whrend die
"*Thread()"-Prozeduren eine Prozekennung bentigen. Der bei Programmstart
aktive Proze wird automatisch angemeldet, so da die Prozeduren
normalerweise (keine Threads) nicht benutzt werden mssen.

Fordert ein Thread, der nicht angemeldet ist, Speicher an, so bekommt er
die Fehlermeldung, da kein Speicher mehr vorhanden ist.

Was fr die Speicherverwaltung auf unterer Ebene gilt, gilt allerdings
auch fr dynamische Datenstrukturen im Programm: Diese stellen praktisch
eine Speicherverwaltung dar, da hierbei von der eigtl. Speicherverwaltung
angeforderte Blcke miteinander verkpft werden. Aus diesem Grund drfen
solche dynamischen Datenstrukturen, z.B. die ADTs aus MISC, nicht von
unterschiedlichen Threads gemeinsam benutzt werden!


Bibliotheksfunktionen, die Speicher anfordern
---------------------------------------------
In M2LIB gibt es auch einige Prozeduren, die dynamisch Speicher anfordern,
weshalb sich Threads, die sie benutzen, anmelden mssen:

o Das ffnen von Kanlen mittels der "*File.Open*()"-Prozeduren aus ISO.

o Benutzung der ADTs aus MISC.

o "dir.opendir()" aus POSIX.
  "mem.strdup()" aus POSIX.

Ein besonderes Problem stellt die Funktion "args.putenv()" dar: Sie benutzt
zwar nicht die Speicherverwaltung, fordert bei Bedarf aber trotzdem Speicher
ber Malloc() an. Nun mu der Speicher, der von dieser Funktion angefordert
wird, fr alle Threads dauerhaft zur Verfgung stehen, da sonst nicht mehr
auf die Environmentvariablen und Programmargumente zugegriffen werden kann;
d.h. der Speicher darf erst bei Programmende freigegeben werden. Die sicherste
Methode wre deshalb, diese Funktion nicht innerhalb von Threads aufzurufen.
Damit diese Mglichkeit aber nicht vllig verlorengeht, wird bei
Programmstart mehr Speicher als ntig fr die Variablen und Argumente
angefordert, so da neuer Speicher erst nach einer, normalerweise ausreichend
hohen, Zahl von "putenv()"-Aufrufen bentigt wird. Auf diese Weise kann
"putenv()" auch innerhalb von Threads aufgerufen werden, solange die Anzahl
der Aufrufe ein gewisses Ma (256) nicht bersteigt.


Verschiedenes
=============
Wenn ein GEM-Programm mit "fork()" einen Unterproze erzeugt, mu sich der
Unterproze wie ein normales GEM-Programm separat vom Hauptproze beim GEM
mittels appl_init()/appl_exit() an- und abmelden. Wird dagegen "vfork()"
oder "tfork()" verwendet, ist dies nicht ntig, da hierbei die
Programmvariablen von den Prozessen gemeinsam benutzt werden.

Ein Beispiel fr eine Signalbehandlung innerhalb von GEM-Programmen
ist XSample aus "crystal". Sobald SIGINT (CTRL-ALT-C), SIGQUIT
(SHIFT-CTRL-ALT-\) oder SIGTERM (Ziehen auf den Desktop-Mlleimer) ausgelst
wird, dauert es hchstens eine Sekunde bis die Abbruch-Alarmbox auftaucht.

Genau genommen haben Threads, die mit "fork()" erzeugt wurden, keine
Probleme mit der Speicherverwaltung, weil Vernderungen an der
Speicherverwaltung des einen Threads keine Auswirkungen auf die der anderen
Threads haben (getrennte Speicherbereiche), sie werden hier jedoch
gleichwertig zu den mit "tfork()" oder "vfork()" erzeugten Threads behandelt.

Bei AES-Versionen < 4.0 drfen ACCs keinen dynamischen Speicher anfordern,
bzw. nur ganz am Anfang vor dem ersten GEM-Aufruf, da bei diesen
AES-Versionen ACCs keine eigenstndigen Prozesse sind und der angeforderte
Speicher der gerade aktiven Hauptapplikation gehrt. Wird diese beendet,
so wird auch automatisch der vom ACC angeforderte Speicher freigegeben.
Es ist dabei insbesondere auf "versteckte" Speicheranforderungen durch
Bibliotheksaufrufe zu achten. Es gibt allerdings die Mglichkeit, mittels
der Funktionen "mem.AddHeap()" oder "mem.NewHeap()" zu Anfang des Programms
einen ausreichend groen Speicherblock anzufordern bzw. eine globale Variable
ausreichender Gre als Speicherblock anzumelden, so da alle weiteren
Speicheranforderungen aus diesem Block erfllt werden knnen und kein
Speicher beim Betriebssystem angefordert werden mu. Das Problem dabei ist,
festzustellen, wieviel Speicher bentigt wird.

Mit der gleichen Methode lassen sich auch Threads verwenden, ohne da diese
explizit an- oder abgemeldet werden mten. Es mu dafr nur beim
Prprozessieren das Makro __THREADSAFE__ ausgeschaltet werden. Solange
der initiale Speicherblock gro genug ist, da alle Speicheranforderungen
aus ihm erfllt werden knnen, ist die Speicherverwaltung trotzdem noch
Thread-fest.
